/*
 * Copyright 2010-2012 Susanta Tewari. <freecode4susant@users.sourceforge.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package genomemap.map;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import commons.util.ExceptionUtil;
import commons.util.GUIUtil;
import edu.uci.ics.jung.algorithms.layout.StaticLayout;
import edu.uci.ics.jung.graph.DelegateForest;
import edu.uci.ics.jung.graph.Forest;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.util.Context;
import edu.uci.ics.jung.visualization.VisualizationViewer;
import edu.uci.ics.jung.visualization.control.CrossoverScalingControl;
import edu.uci.ics.jung.visualization.control.DefaultModalGraphMouse;
import edu.uci.ics.jung.visualization.decorators.EdgeShape;
import edu.uci.ics.jung.visualization.util.VertexShapeFactory;
import org.apache.commons.collections15.Predicate;
import org.apache.commons.collections15.Transformer;
import org.apache.commons.collections15.TransformerUtils;
import test.genomemap.data.GenomemapTestData;

import javax.swing.*;
import java.awt.*;
import java.awt.geom.Point2D;
import java.util.*;
import java.util.List;

/**
 * @author Susanta Tewari
 * @version version
 * @history Created on 11/1/13.
 * @since since
 */
public class AlignmentMap {

    /** labels in {@code maps} w/o order */
    private final Collection<String> commonSet;

    /** labels in {@code maps} w/ order */
    private Map<String, List<String>> maps = new HashMap<>();
    private JComponent comp;


    // location parameters
    private static final int X_INDENT     = 10;
    private static final int Y_INDENT     = 10;
    private static final int LABEL_WIDTH  = 40;
    private static final int LABEL_HEIGHT = 60;    // 1.5 TIMES
    private static final int X_GAP        = 1000;
    private static final int Y_GAP        = 20;

    /**
     * Could be used to avoid {@code MapException} in {@link #AlignmentMap(java.util.List, boolean)}.
     *
     * @param maps genome maps (at least 2)
     * @return the common set among the given maps
     */
    public static Collection<String> commonsSet(final List<? extends GenomeMap> maps) {

        ExceptionUtil.throwArgEx(maps.size() < 2, "at least 2 maps");

        final Collection<String> commonSet = new HashSet<>(maps.get(0).getLabels());

        for (int i = 0; i < maps.size(); i++) {
            commonSet.retainAll(maps.get(i).getLabels());
        }

        return commonSet;
    }

    /**
     * @param maps alignment maps
     * @param commonSet common labels in {@code maps}, can be created via
     * {@link #commonsSet(java.util.List)}
     * @throws MapException if the common data is empty
     */
    public AlignmentMap(final List<? extends GenomeMap> maps, Collection<String> commonSet)
            throws MapException {

        ExceptionUtil.throwArgEx(maps.size() < 2, "at least 2 maps");

        if (commonSet.size() == 0) throw new MapException("common set is empty");

        this.commonSet = ImmutableSet.copyOf(commonSet);

        for (GenomeMap map : maps) {    // common map in order for each

            final List<String> result = new ArrayList<>(map.getLabels());

            result.retainAll(commonSet);
            this.maps.put(map.getTitle(), result);
        }
    }

    /**
     * @return list of cross-counts (immutable)
     */
    public int getCrossCounts(String mapTitle1, String mapTitle2) {

        final List<String> map1 = maps.get(mapTitle1);
        final List<String> map0 = maps.get(mapTitle2);
        int count               = 0;

        for (String s : commonSet) {
            if (map1.indexOf(s) != map0.indexOf(s)) count++;
        }

        return count;
    }

    public JComponent getComponent() {

        if (comp == null) comp = createComp();

        return comp;
    }

    public static void main(String[] args) {

        final List<GenomeMap> maps = new ArrayList<>(2);

        maps.add(GenomemapTestData.dataSet.get("D1"));
        maps.add(GenomemapTestData.dataSet.get("D2"));

        final AlignmentMap alignmentMap = new AlignmentMap(maps, commonsSet(maps));

        GUIUtil.displayGUI("", alignmentMap.getComponent());
    }

    public List<String> uncommonSet(GenomeMap map) {

        final List<String> result = new ArrayList<>(map.getLabels());

        result.removeAll(commonSet);

        return result;
    }

    private JComponent createComp() {

        Forest<V, V> forest           = new DelegateForest<>();
        Map<String, List<V>> nodeMaps = new HashMap<>();

        for (String mapTitle : maps.keySet()) {

            final List<String> map = maps.get(mapTitle);
            final List<V> nodes    = new ArrayList<>();


            // map title
            final V v_title = V.of_Title(mapTitle);

            forest.addVertex(v_title);
            nodes.add(v_title);


            // labels: add vertices
            for (String s : map) {

                final V a = V.of_Label(s);

                nodes.add(a);
                forest.addVertex(a);
            }


            // intra map: add edges
            for (int i = 2; i < nodes.size(); i++) {

                final V a = nodes.get(i - 1);
                final V b = nodes.get(i);

                forest.addEdge(V.of_ONE(), a, b);
            }

            nodeMaps.put(mapTitle, nodes);
        }


        // inter map: add crossings
        final List<String> titles = Lists.newArrayList(maps.keySet());

        for (String s : commonSet) {

            for (int i = 1; i < titles.size(); i++) {

                final String t1 = titles.get(i - 1);
                final String t2 = titles.get(i);
                final int i1    = maps.get(t1).indexOf(s);
                final int i2    = maps.get(t2).indexOf(s);


                // inter map
                final V v1 = nodeMaps.get(t1).get(i1 + 1);    // 1: accounting title
                final V v2 = nodeMaps.get(t2).get(i2 + 1);

                forest.addEdge(V.of_TWO(), v1, v2);
            }
        }


        // positions
        Map<V, Point2D> points = new HashMap<>();

        for (int i = 0; i < titles.size(); i++) {

            String title  = titles.get(i);
            List<V> vList = nodeMaps.get(title);
            int x         = X_INDENT + LABEL_WIDTH + i * X_GAP;
            int y         = Y_INDENT;

            points.put(vList.get(0), new Point2D.Double(x, y));

            for (int i1 = 0; i1 < vList.size(); i1++) {

                V v = vList.get(i1);

                y += LABEL_HEIGHT + Y_GAP;

                points.put(v, new Point2D.Double(x, y));
            }
        }


        // comp
        StaticLayout layout = new StaticLayout<>(forest, TransformerUtils.mapTransformer(points));
        final VisualizationViewer<V, V> vv = new VisualizationViewer<V, V>(layout);
        final int maxLength                = Y_INDENT + (LABEL_HEIGHT + Y_GAP) * commonSet.size();


        // modify vv
        modifyVV(vv, maxLength);

        return vv;
    }

    private void modifyVV(final VisualizationViewer<V, V> vv, double maxLength) {


        // background
        vv.setBackground(Color.white);


        // adjust vertex label
        vv.getRenderContext().setVertexLabelTransformer(new Transformer<V, String>() {

            @Override
            public String transform(final V node) {
                return node.isTitle() ? node.getLabel() : null;
            }

        });


        // vertex tooltip
        vv.setVertexToolTipTransformer(new Transformer<V, String>() {

            @Override
            public String transform(V input) {
                return input.getLabel();
            }

        });


        // adjust vertex color
        vv.getRenderContext().setVertexFillPaintTransformer(new Transformer<V, Paint>() {

            @Override
            public Paint transform(final V node) {
                return node.isTitle() ? null : Color.gray;
            }

        });


        // adjust vertex size
        final float A_RATIO = ((float) LABEL_HEIGHT) / LABEL_WIDTH;
        final VertexShapeFactory vertexShapeFactory = new VertexShapeFactory(new Transformer<V,
                                                          Integer>() {

            @Override
            public Integer transform(final V node) {

                if (!node.isTitle()) return LABEL_WIDTH;

                return vv.getFontMetrics(vv.getFont()).stringWidth(node.getLabel()) + 15;
            }

        }, new Transformer<V, Float>() {

            @Override
            public Float transform(final V node) {
                return node.isTitle() ? 1.0F : A_RATIO;
            }

        });

        vv.getRenderContext().setVertexShapeTransformer(new Transformer<V, Shape>() {

            @Override
            public Shape transform(final V node) {
                return vertexShapeFactory.getRectangle(node);
            }

        });


        // vertex label position
        vv.getRenderer().getVertexLabelRenderer().setPosition(
            edu.uci.ics.jung.visualization.renderers.Renderer.VertexLabel.Position.CNTR);


        // edge
        vv.getRenderContext().setEdgeShapeTransformer(new EdgeShape.Line());
        vv.getRenderContext().setEdgeArrowPredicate(new Predicate<Context<Graph<V, V>, V>>() {

            @Override
            public boolean evaluate(Context<Graph<V, V>, V> object) {
                return false;
            }

        });
        vv.getRenderContext().setEdgeIncludePredicate(new Predicate<Context<Graph<V, V>, V>>() {

            @Override
            public boolean evaluate(Context<Graph<V, V>, V> object) {
                return true;
            }

        });
        vv.getRenderContext().setEdgeDrawPaintTransformer(new Transformer<V, Paint>() {

            @Override
            public Paint transform(V input) {
                return input.isEdgeOne() ? Color.black : Color.red;
            }

        });
        vv.getRenderContext().setEdgeFillPaintTransformer(new Transformer<V, Paint>() {

            @Override
            public Paint transform(V input) {
                return input.isEdgeOne() ? Color.black : Color.red;
            }

        });
        vv.getRenderContext().setEdgeStrokeTransformer(new Transformer<V, Stroke>() {

            @Override
            public Stroke transform(V input) {
                return new BasicStroke(4.0f);
            }

        });


        // mouse
        final DefaultModalGraphMouse graphMouse = new DefaultModalGraphMouse();

        graphMouse.setMode(DefaultModalGraphMouse.Mode.TRANSFORMING);
        vv.setGraphMouse(graphMouse);


        // scaling: fit screen
        final double factor  = (vv.getSize().getHeight()) / (1.3 * maxLength);
        final double centerX = vv.getCenter().getX();

        new CrossoverScalingControl().scale(vv, (float) factor, new Point2D.Double(centerX, 5));
    }

    private static class V {    // node

        final static String ONE = "1",
                            TWO = "2";

        /**
         * 1 = title
         * 2 = labels
         * 3 = edge
         */
        private int type;
        private String label;

        boolean isTitle() {
            return type == 1;
        }

        boolean isLabel() {
            return type == 2;
        }

        boolean isEdge() {
            return type == 3;
        }

        boolean isEdgeOne() {
            return (type == 3) && (label == ONE);
        }

        boolean isEdgeTwo() {
            return (type == 3) && (label == TWO);
        }

        private V(String label, int type) {

            this.label = label;
            this.type  = type;
        }

        String getLabel() {
            return label;
        }

        static V of_ONE() {
            return new V(ONE, 3);
        }

        static V of_TWO() {
            return new V(TWO, 3);
        }

        static V of_Label(String s) {
            return new V(s, 2);
        }

        static V of_Title(String s) {
            return new V(s, 1);
        }
    }
}
